package top.lishuoboy.redis.controller;

import cn.hutool.core.codec.Base64Decoder;
import cn.hutool.core.codec.Base64Encoder;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.DataType;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import redis.clients.jedis.*;
import redis.clients.jedis.params.GeoAddParams;
import redis.clients.jedis.params.GeoRadiusParam;
import redis.clients.jedis.params.SetParams;
import top.lishuoboy.dependency.base.json.HuJsonUtil;

import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * @author lishuoboy
 * @date 2022/3/2 12:27
 */
@Slf4j
@RestController
public class JedisController {
    @Autowired
    private Jedis jedis;
    @Autowired
    private JedisPool jedisPool;
    @Autowired
    private StringRedisTemplate template;
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private RedissonClient redissonClient1;
//    @Autowired
//    private RedissonClient redissonClient2;
//    @Autowired
//    private RedissonClient redissonClient3;

    @GetMapping("getJedis")
    private Object getJedis() {
        log.warn("jedis.ping()=={}", jedis.ping());
        log.warn("jedis.isConnected()=={}", jedis.isConnected());
        return jedis.ping();
    }

    @GetMapping("jedisPool")
    private void jedisPool() {
        for (int i = 0; i < 20000; i++) {
            Jedis jedis = jedisPool.getResource();
            System.out.println(i + "@" + jedis.hashCode());
//            jedis.close();
        }
    }

    /** jedis */
    private void resetDb() {
        jedis.flushDB();

        jedis.set("a1str", "张三");
        jedis.lpush("a2list", "张三", "李四");
        jedis.sadd("a3set", "张三", "李四");
        jedis.hset("a4hash", MapUtil.builder("field1", "张三").put("field2", "李四").build());
        jedis.zadd("a5zset", MapUtil.builder("张三", 1.0).put("李四", 2d).build());

        jedis.set("key1", "value1");
        jedis.set("key2", "value2");
        jedis.set("key3", "value3");

    }

    /** StringRedisTemplate */
    @GetMapping("resetDb2")
    private void resetDb2() {
        RedisConnection redisConnection = template.getConnectionFactory().getConnection();
        redisConnection.flushDb();
        template.opsForValue().set("a1str", "张三");
        template.opsForList().leftPushAll("a2list", "张三", "李四");
        template.opsForSet().add("a3set", "张三", "李四");
        template.opsForHash().putAll("a4hash", MapUtil.builder("field1", "张三").put("field2", "李四").build());
        template.opsForZSet().add("a5zset", "张三", 1.0);
        template.opsForZSet().add("a5zset", "李四", 2d);

        template.opsForValue().set("key1", "value1");
        template.opsForValue().set("key2", "value2");
        template.opsForValue().set("key3", "value3");
    }

    @GetMapping("jedisKey")
    private void jedisKey() {
        resetDb();

        //1、查
        Set<String> keySet;
        keySet = jedis.keys("*");
        log.warn("keySet.size()=={},  keySet=={}", keySet.size(), CollectionUtil.sortByPinyin(keySet));  // keySet.size()==8,  keySet==[a1str,  a2list,  a3set,  a4hash,  a5zset,  k1,  k2,  k3]
        keySet = jedis.keys("*s*");
        log.warn("keySet.size()=={},  keySet=={}", keySet.size(), CollectionUtil.sortByPinyin(keySet));  // keySet.size()==5,  keySet==[a1str,  a2list,  a3set,  a4hash,  a5zset]
        // 判断某个key是否存在, 是否存在 或者 存在个数
        boolean isExists = jedis.exists("a1str");
        log.warn("isExists=={}", isExists);         // isExists==true
        long existsCount = jedis.exists("key1", "key2", "key8"); //存在个数
        log.warn("existsCount=={}", existsCount);   //existsCount==2
        //type key      查看你的 key是什么类型
        String type;
        type = jedis.type("a1str");     //type==string
        log.warn("type=={}", type);
        type = jedis.type("a2list");    //type==list
        log.warn("type=={}", type);
        type = jedis.type("a3set");     //type==set
        log.warn("type=={}", type);
        type = jedis.type("a4hash");    //type==hash
        log.warn("type=={}", type);
        type = jedis.type("a5zset");    //type==zset
        log.warn("type=={}", type);

        //2、删
        //del key       删除指定的key数据。返回long删除数量
        long delCnt = jedis.del("a2list", "a3set");
        log.warn("delCnt=={}", delCnt);         //delCnt==2
        //unlink key    根据value选择非阻塞删除。仅将keys 从 keyspace 元数据中删除, 真正的删除会在后续异步操作。返回long删除数量
        long unlinkCnt = jedis.unlink("a4hash", "a5zset");
        log.warn("unlinkCnt=={}", unlinkCnt);   //unlinkCnt==2
        //expire/pexpire key seconds    为给定的key设置过期时间。返回int, 1：设置了超时。 0：未设置超时
        long expire = jedis.expire("a1str", 10);
        log.warn("expire=={}", expire);         //expire==1
        //expireAt/pexpireAt key timestamp 为给定的key设置过期时间。timestamp单位为秒。返回int, 1：设置了超时。 0：未设置超时
        long timeStrap = (System.currentTimeMillis() + 10 * 1000) / 1000;
        long expireAt = jedis.expireAt("key1", timeStrap);
        log.warn("expireAt=={}", expireAt);         //expireAt==1
        //ttl/pttl key       查看还有多少秒过期, >=0表示剩余过期时间, -1表示永不过期, -2表示已过期
        long ttl = jedis.ttl("a1str");
        log.warn("ttl=={}", ttl);           //ttl==10
        ttl = jedis.ttl("key1");
        log.warn("ttl=={}", ttl);           //ttl==9
        //dbsize        查看当前数据库的 key的数量, 返回long
        long dbSize = jedis.dbSize();
        log.warn("dbSize=={}", dbSize);     //dbSize==4
    }

    @GetMapping("templateKey")
    private void templateKey() {
        resetDb2();

        //1、查
        Set<String> keySet;
        keySet = template.keys("*");
        log.warn("keySet.size()=={},  keySet=={}", keySet.size(), CollectionUtil.sortByPinyin(keySet));  // keySet.size()==8,  keySet==[a1str,  a2list,  a3set,  a4hash,  a5zset,  k1,  k2,  k3]
        keySet = template.keys("*s*");
        log.warn("keySet.size()=={},  keySet=={}", keySet.size(), CollectionUtil.sortByPinyin(keySet));  // keySet.size()==5,  keySet==[a1str,  a2list,  a3set,  a4hash,  a5zset]
        // 判断某个key是否存在, 是否存在 或者 存在个数
        boolean isExists = template.hasKey("a1str");
        log.warn("isExists=={}", isExists);         // isExists==true
        long existsCount = template.countExistingKeys(Arrays.asList("key1", "key2", "key8")); //存在个数
        log.warn("existsCount=={}", existsCount);   //existsCount==2
        //type key      查看你的 key是什么类型
        DataType type;
        type = template.type("a1str");     //type==string
        log.warn("type=={}", type);
        type = template.type("a2list");    //type==list
        log.warn("type=={}", type);
        type = template.type("a3set");     //type==set
        log.warn("type=={}", type);
        type = template.type("a4hash");    //type==hash
        log.warn("type=={}", type);
        type = template.type("a5zset");    //type==zset
        log.warn("type=={}", type);

        //2、删
        //del key       删除指定的key数据。返回long删除数量
        long delCnt = template.delete(Arrays.asList("a2list", "a3set"));
        log.warn("delCnt=={}", delCnt);         //delCnt==2
        //unlink key    根据value选择非阻塞删除。仅将keys 从 keyspace 元数据中删除, 真正的删除会在后续异步操作。返回long删除数量
        long unlinkCnt = template.unlink(Arrays.asList("a4hash", "a5zset"));
        log.warn("unlinkCnt=={}", unlinkCnt);   //unlinkCnt==2
        //expire/pexpire key seconds    为给定的key设置过期时间。返回int, 1：设置了超时。 0：未设置超时
        boolean expire = template.expire("a1str", 10, TimeUnit.SECONDS);
        log.warn("expire=={}", expire);         //expire==true
        //expireAt/pexpireAt key timestamp 为给定的key设置过期时间。timestamp单位为秒。返回int, 1：设置了超时。 0：未设置超时
        long timeStrap = (System.currentTimeMillis() + 10 * 1000);
        boolean expireAt = template.expireAt("key1", new Date(timeStrap));
        log.warn("expireAt=={}", expireAt);         //expireAt==true
        //ttl/pttl key       查看还有多少秒过期, >=0表示剩余过期时间, -1表示永不过期, -2表示已过期
        long ttl = template.getExpire("a1str");
        log.warn("ttl=={}", ttl);           //ttl==10
        ttl = template.getExpire("key1");
        log.warn("ttl=={}", ttl);           //ttl==9
        //dbsize        查看当前数据库的 key的数量, 返回long
        long dbSize = template.getConnectionFactory().getConnection().dbSize();
        log.warn("dbSize=={}", dbSize);     //dbSize==4
    }

    @GetMapping("jedisString")
    private void jedisString() {
        resetDb();

        //1、增改
        //set <key><value>               添加键值对, 返回状态码 OK/nil(null)
        //                                   *NX:当数据库中key不存在时, 可以将key-value添加数据库
        //                                   *XX:当数据库中key  存在时, 可以将key-value添加数据库, 与参数NX互斥
        //                                   *EX: key的超时秒数
        //                                   *PX: key的超时毫秒数, 与EX互斥
        String resCode = jedis.set("k1", "v1");
        log.warn("resCode=={}", resCode);     //resCode==OK
        resCode = jedis.set("k1", "v11", new SetParams().nx());
        log.warn("resCode=={}", resCode);     //resCode==null
        resCode = jedis.set("k22", "v22", new SetParams().nx());
        log.warn("resCode=={}", resCode);     //resCode==OK
        //setnx <key><value>                只有在key不存在时设置key的值。返回成功数量
        long count = jedis.setnx("k1", "v11");
        log.warn("count=={}", count);     //count==0
        count = jedis.setnx("k3", "v3");
        log.warn("count=={}", count);     //count==1

        //2、查
        //get <key>                             查询对应键值
        String k1 = jedis.get("k1");
        log.warn("k1=={}", k1);     //k1==v1

        //3、查改
        //getset <key><value>                   以新换旧, 设置了新值同时获得旧值。
        k1 = jedis.getSet("k1", "v11");
        log.warn("k1=={}", k1);     //k1==v1
        //4、批量 增 改 查
        //mset <keyl><valuel><key2><value2>     同时设置一个或多个key-value对。返回状态码 OK/nil(null)
        resCode = jedis.mset("k3", "v3", "k4", "v4");
        log.warn("resCode=={}", resCode);     //resCode==OK
        //mget <key1><key2><key3>               同时获取一个或多个value
        List<String> mget = jedis.mget("k3", "k4");
        log.warn("mget=={}", mget);     //mget==[v3,  v4]
        //msetnx <key1><value1><key2><value2> 同时设置一个或多个key-value 对, 当且仅当所有给定key都不存在。原子性, 有一个失败则都失败。返回整数, 1 如果设置了所有键 0 如果没有设置键（至少一个键已经存在）
        long msetnx = jedis.msetnx("k3", "v33", "k4", "v44");
        log.warn("msetnx=={}", msetnx);     //msetnx==0
        msetnx = jedis.msetnx("k5", "v5", "k6", "v6");
        log.warn("msetnx=={}", msetnx);     //msetnx==1
        //5、函数
        //append <key><value>         将给定的<value>追加到原值的末尾。返回append后的长度
        long length = jedis.append("k1", "1111111111");
        log.warn("length=={}", length);     //length==13
        //strlen <key>                获得值的长度
        length = jedis.strlen("k1");
        log.warn("length=={}", length);     //length==13
        //getrange <key><起始位置><结束位置>      获得值的范围, 类似java中的substring, 前包, 后包
        String k1Range = jedis.getrange("k1", 0, 5);
        log.warn("k1Range=={}", k1Range);     //k1Range==v11111
        //setrange <key><起始位置><value>     用<value>覆写<key>所储存的字符串值, 从<起始位置>开始(索引从О开始)。返回set后的长度
        length = jedis.setrange("k1", 1, "5");
        log.warn("length=={}", length);     //length==13
        log.warn("k1=={}", jedis.get("k1"));     //k1==v511111111111
        //6、增、减
        //incr <key>                  将key中储存的数字值增1。只能对数字值操作, 如果为空, 新增值为1。返回incr后的值
        jedis.set("k7", "1");
        long val = jedis.incr("k7");
        log.warn("val=={}", val);     //val==2
        //decr <key>                  将key中储存的数字值减1。只能对数字值操作, 如果为空, 新增值为-1
        //incrby / decrby <key><步长>   将 key 中储存的数字值增减。自定义步长。返回incrby后的值
        val = jedis.decrBy("k7", 10);
        log.warn("val=={}", val);     //val==-8
    }

    @GetMapping("templateString")
    private void templateString() {
        resetDb2();

        //1、增改
        //set <key><value>               添加键值对, 返回状态码 OK/nil(null)
        //                                   *NX:当数据库中key不存在时, 可以将key-value添加数据库
        //                                   *XX:当数据库中key  存在时, 可以将key-value添加数据库, 与参数NX互斥
        //                                   *EX: key的超时秒数
        //                                   *PX: key的超时毫秒数, 与EX互斥
        template.opsForValue().set("k1", "v1");
//        log.warn("resCode=={}",  resCode);     //
        boolean success = template.opsForValue().setIfAbsent("k1", "v11");
        log.warn("success=={}", success);     //success==false
        success = template.opsForValue().setIfAbsent("k22", "v22");
        log.warn("success=={}", success);     //success==true
        //setnx <key><value>                只有在key不存在时设置key的值。返回成功数量
        success = template.opsForValue().setIfAbsent("k1", "v11");
        log.warn("success=={}", success);     //success==false
        success = template.opsForValue().setIfAbsent("k3", "v3");
        log.warn("success=={}", success);     //success==true

        //2、查
        //get <key>                             查询对应键值
        String k1 = template.opsForValue().get("k1");
        log.warn("k1=={}", k1);     //k1==v1

        //3、查改
        //getset <key><value>                   以新换旧, 设置了新值同时获得旧值。
        k1 = template.opsForValue().getAndSet("k1", "v11");
        log.warn("k1=={}", k1);     //k1==v1
        //4、批量 增 改 查
        //mset <keyl><valuel><key2><value2>     同时设置一个或多个key-value对。返回状态码 OK/nil(null)
        template.opsForValue().multiSet(MapUtil.builder("k3", "v3").put("k4", "v4").build());
//        log.warn("resCode=={}",  resCode);     //resCode==OK
        //mget <key1><key2><key3>               同时获取一个或多个value
        List<String> mget = template.opsForValue().multiGet(Arrays.asList("k3", "k4"));
        log.warn("mget=={}", mget);     //mget==[v3,  v4]
        //msetnx <key1><value1><key2><value2> 同时设置一个或多个key-value 对, 当且仅当所有给定key都不存在。原子性, 有一个失败则都失败。返回整数, 1 如果设置了所有键 0 如果没有设置键（至少一个键已经存在）
        boolean msetnx = template.opsForValue().multiSetIfAbsent(MapUtil.builder("k3", "v33").put("k4", "v44").build());
        log.warn("msetnx=={}", msetnx);     //msetnx==false
        msetnx = template.opsForValue().multiSetIfAbsent(MapUtil.builder("k5", "v5").put("k6", "v6").build());
        log.warn("msetnx=={}", msetnx);     //msetnx==true
        //5、函数
        //append <key><value>         将给定的<value>追加到原值的末尾。返回append后的长度
        long length = template.opsForValue().append("k1", "1111111111");
        log.warn("length=={}", length);     //length==13
        //strlen <key>                获得值的长度
        length = template.opsForValue().get("k1").length();
        log.warn("length=={}", length);     //length==13
        //getrange <key><起始位置><结束位置>      获得值的范围, 类似java中的substring, 前包, 后包
        String k1Range = template.opsForValue().get("k1", 0, 5);
        log.warn("k1Range=={}", k1Range);     //k1Range==v11111
        //setrange <key><起始位置><value>     用<value>覆写<key>所储存的字符串值, 从<起始位置>开始(索引从О开始)。返回set后的长度
        template.opsForValue().set("k1", "5", 1);
//        log.warn("length=={}",  length);     //length==13
        log.warn("k1=={}", template.opsForValue().get("k1"));     //k1==v511111111111
        //6、增、减
        //incr <key>                  将key中储存的数字值增1。只能对数字值操作, 如果为空, 新增值为1。返回incr后的值
        template.opsForValue().set("k7", "1");
        long val = template.opsForValue().increment("k7");
        log.warn("val=={}", val);     //val==2
        //decr <key>                  将key中储存的数字值减1。只能对数字值操作, 如果为空, 新增值为-1
        //incrby / decrby <key><步长>   将 key 中储存的数字值增减。自定义步长。返回incrby后的值
        val = template.opsForValue().decrement("k7", 10);
        log.warn("val=={}", val);     //val==-8
    }

    /** 存取 bean json file */
    @GetMapping("jedisStringObj")
    private void jedisStringObj() {
        resetDb();

        String resCode;

        Map<String, Object> map = new LinkedHashMap();
        map.put("id", 111);
        map.put("name", "张三");

        String myBeanStr = map.toString();
        String myJsonStr = HuJsonUtil.toJsonStr(map);
        log.warn("myBeanStr=={}", myBeanStr);     // {id=111,  name=张三}
        log.warn("myJsonStr=={}", myJsonStr);     // {"id":111, "name":"张三"}


        // 存入格式为 plain text
        resCode = jedis.set("myBeanStr", myBeanStr);
        log.warn("resCode=={}", resCode);     // resCode==OK

        // 存入格式为 json
        resCode = jedis.set("myJsonStr", myJsonStr);
        log.warn("resCode=={}", resCode);     // resCode==OK


        resCode = jedis.set("myTextFile", ResourceUtil.readUtf8Str("application.properties"));
        log.warn("resCode=={}", resCode);     // resCode==OK

        resCode = jedis.set("myImgFile".getBytes(StandardCharsets.UTF_8), ResourceUtil.readBytes("tmp/img.png"));
        log.warn("resCode=={}", resCode);     // resCode==OK

        // 写出的文件在 target/classes 目录
        String myTextFileStr = jedis.get("myTextFile");
        IoUtil.write(FileUtil.getOutputStream("tmp/application_副本.properties"), true, myTextFileStr.getBytes(StandardCharsets.UTF_8));

        byte[] myImgFileBytes = jedis.get("myImgFile".getBytes(StandardCharsets.UTF_8));
        IoUtil.write(FileUtil.getOutputStream("tmp/img_副本.png"), true, myImgFileBytes);
    }

    /** 存取 bean json file */
    @GetMapping("templateStringObj")
    private void templateStringObj() {
        resetDb2();

        String resCode;

        Map<String, Object> map = new LinkedHashMap();
        map.put("id", 111);
        map.put("name", "张三");

        String myBeanStr = map.toString();
        String myJsonStr = HuJsonUtil.toJsonStr(map);
        log.warn("myBeanStr=={}", myBeanStr);     // {id=111,  name=张三}
        log.warn("myJsonStr=={}", myJsonStr);     // {"id":111, "name":"张三"}


        // 存入格式为 plain text
        template.opsForValue().set("myBeanStr2", myBeanStr);
//        log.warn("resCode=={}",  resCode);     // resCode==OK
//        Map myBeanStrMap = BeanUtil.toBean(template.opsForValue().get("myBeanStr2"), Map.class);
//        log.warn("myBeanStrMap=={}", myBeanStrMap);         // myBeanStrMap=={}

        // 存入格式为 json
        template.opsForValue().set("myJsonStr2", myJsonStr);
//        log.warn("resCode=={}",  resCode);     // resCode==OK
        Map myBeanJsonMap = HuJsonUtil.toBean(template.opsForValue().get("myJsonStr2"), Map.class);
        log.warn("myBeanJsonMap=={}", myBeanJsonMap);       // myBeanJsonMap=={id=111, name=张三}


        redisTemplate.opsForValue().set("myBean", map);
        Map myBeanMap = (Map) redisTemplate.opsForValue().get("myBean");
        log.warn("myBeanMap=={}", myBeanMap);               // myBeanMap=={id=111, name=张三}

        template.opsForValue().set("myTextFile2", ResourceUtil.readUtf8Str("application.properties"));
//        log.warn("resCode=={}",  resCode);     // resCode==OK

        template.opsForValue().set("myImgFile2", Base64Encoder.encode(ResourceUtil.readBytes("tmp/img.png")));
//        log.warn("resCode=={}",  resCode);     // resCode==OK

        // Spring 注入的 RedisTemplate 的 Serializer 都使用 JDK 的序列化。可读性差，占用空间大
        log.debug("Serializer=={}", redisTemplate.getKeySerializer().getClass());       // Serializer==class org.springframework.data.redis.serializer.JdkSerializationRedisSerializer
        log.debug("Serializer=={}", redisTemplate.getValueSerializer().getClass());     // Serializer==class org.springframework.data.redis.serializer.JdkSerializationRedisSerializer
        /** 修改对应的序列化器 */
        redisTemplate.setKeySerializer(RedisSerializer.string());               // 修改 Key       的序列化为 String
        redisTemplate.setValueSerializer(RedisSerializer.byteArray());          // 修改 Value     的序列化为 byte[]
        log.debug("Serializer=={}", redisTemplate.getKeySerializer().getClass());       // Serializer==class org.springframework.data.redis.serializer.ByteArrayRedisSerializer
        log.debug("Serializer=={}", redisTemplate.getValueSerializer().getClass());     // Serializer==class org.springframework.data.redis.serializer.ByteArrayRedisSerializer

        redisTemplate.opsForValue().set("myImgFile3", ResourceUtil.readBytes("tmp/img.png"));
//        log.warn("resCode=={}",  resCode);     // resCode==OK

        // 写出的文件在 target/classes 目录
        String myTextFileStr = template.opsForValue().get("myTextFile2");
        IoUtil.write(FileUtil.getOutputStream("tmp/application_副本2.properties"), true, myTextFileStr.getBytes(StandardCharsets.UTF_8));

        String myImgFileStr = template.opsForValue().get("myImgFile2");
        IoUtil.write(FileUtil.getOutputStream("tmp/img_副本2.png"), true, Base64Decoder.decode(myImgFileStr));

        byte[] myImgFile3 = (byte[]) redisTemplate.opsForValue().get("myImgFile3");
        IoUtil.write(FileUtil.getOutputStream("tmp/img_副本3.png"), true, myImgFile3);
    }

    @GetMapping("jedisList")
    private void jedisList() {
        resetDb();

        // 1、增
        //lpush/rpush <key> ...<values>             从左边（倒序）/右边（正序）插入一个或多个值。返回push后list的value的个数
        long listCount = jedis.lpush("k1", "v1", "v2", "v4");
        log.warn("listCount=={}", listCount);       //listCount==3
        //linsert <key> before/after <value><newalue> 在<value>的前面/后面插入<newvalue>插入值。返回insert后list的value的个数
        listCount = jedis.linsert("k1", ListPosition.BEFORE, "v2", "v3");
        log.warn("listCount=={}", listCount);       //listCount==4

        // 2、查
        //lrange <key><start><stop>                 按照索引下标获得元素(从左到右)。lrange <key> 0 -1  0左边第一个, -1右边第一个, (0-1表示获取所有)
        List<String> k1List = jedis.lrange("k1", 0, 2);
        log.warn("k1List=={}", k1List);             //k1List==[v4,  v3,  v2]
        List<String> k2List = jedis.lrange("k1", 0, -1);
        log.warn("k2List=={}", k2List);             //k2List==[v4,  v3,  v2,  v1]
        //lindex <key><index>                       按照索引下标获得元素(从左到右)
        String k1_0 = jedis.lindex("k1", 0);
        log.warn("k1_0=={}", k1_0);                 //k1_0==v4
        String k1_2 = jedis.lindex("k1", 2);
        log.warn("k1_2=={}", k1_2);                 //k1_2==v2
        //lpop/rpop <key>                            从左边/右边删除一个值。值在键在, 值光键亡。返回吐出的值
        String k1pop = jedis.lpop("k1");
        log.warn("k1pop=={}", k1pop);               //k1pop==v4
        // 3、改
        //lset<key><index><value>                    将列表key下标为index的值替换成value。返回状态
        String resCode = jedis.lset("k1", 0, "v33");
        log.warn("resCode=={}", resCode);           //resCode==OK
        // 4、删
        //lrem <key><n><value>                       从左边删除n个value(从左到右), 返回实际删除个数
        long lremCount = jedis.lrem("k1", 2, "v3");
        log.warn("lremCount=={}", lremCount);       //lremCount==1
        // 5、函数
        //llen <key>                                获得列表长度
        long length = jedis.llen("k1");
        log.warn("length=={}", length);             //length==2
        //rpoplpush <key1><key2>                    从<key1>列表右边吐出一个值, 插到<key2>列表左边。返回挪动的值
        String rpoplpush = jedis.rpoplpush("k1", "k2");
        log.warn("rpoplpush=={}", rpoplpush);       //rpoplpush==v1
        k1List = jedis.lrange("k1", 0, -1);
        log.warn("k1List=={}", k1List);             //k1List==[v33]
        k2List = jedis.lrange("k2", 0, -1);
        log.warn("k2List=={}", k2List);             //k2List==[v1]
    }

    @GetMapping("templateList")
    private void templateList() {
        resetDb2();
        // 略
    }

    @GetMapping("jedisSet")
    private void jedisSet() {
        resetDb();

        // 1、增改
        //sadd <key><value1><value2>     将一个或多个 member 元素加入到集合 key 中, 已经存在的 member 元素将被忽略。返回 add成功的个数
        long addCount = jedis.sadd("k1", "v1", "v2", "v3", "v3");
        log.warn("addCount=={}", addCount);                 //addCount==3
        addCount = jedis.sadd("k2", "v2", "v3", "v4", "v5");
        log.warn("addCount=={}", addCount);                 //addCount==4
        // 2、查
        //smembers <key>                 取出该集合的所有值。
        Set<String> set = jedis.smembers("k1");
        log.warn("set=={}", set);                 //set==[v1,  v2,  v3,  v4,  v5]
        //sismember <key><value>         判断集合<key>是否为含有该<value>值, 有 true, 没有 false
        boolean ismember1 = jedis.sismember("k1", "v3");
        log.warn("ismember1=={}", ismember1);                 //ismember1==true
        boolean ismember2 = jedis.sismember("k1", "v8");
        log.warn("ismember2=={}", ismember2);                 //ismember2==false
        //scard<key>                     返回该集合的元素个数
        long count = jedis.scard("k1");
        log.warn("count=={}", count);                 //count==5
        //srandmember <key><n>           随机从该集合中取出 n 个值。不会从集合中删除 。
        List<String> stringList = jedis.srandmember("k1", 2);
        log.warn("stringList=={}", stringList);                 //stringList==[v2,  v4]
        // 3、删
        //spop <key>                     随机从该集合中吐出一个值, 并删除
//        String k1Pop = jedis.spop("k1");
//        log.warn("k1Pop=={}",  k1Pop);                 //k1Pop==v4
        //srem <key><value1><value2>     删除集合中的某个元素。返回删除个数
        long sremCount = jedis.srem("k1", "v4", "v5");
        log.warn("sremCount=={}", sremCount);                 //sremCount==2
        // 4 跨set移动
        //smove <sourceKey><destKey>value 把集合中一个值从一个集合移动到另一个集合, 返回1：成功移动大于等于1个, 0：没有移动
        long smove = jedis.smove("k1", "k2", "v2");
        log.warn("smove=={}", smove);                 //smove1==1
        // 5、交集、并集、差集
        //sinter <key1><key2>            返回两个集合的交集元素。
        set = jedis.sinter("k1", "k2");
        log.warn("set=={}", set);                 //set==[v3]
        //sunion <key1><key2>            返回两个集合的并集元素。
        set = jedis.sunion("k1", "k2");
        log.warn("set=={}", set);                 //set==[v1,  v2,  v3,  v4,  v5]
        //sdiff <key1><key2>             返回两个集合的差集元素(key1 中的, 不包含 key2 中的)
        set = jedis.sdiff("k1", "k2");
        log.warn("set=={}", set);                 //set==[v1]
    }

    @GetMapping("templateSet")
    private void templateSet() {
        resetDb2();
        // 略
    }

    @GetMapping("jedisHash")
    private void jedisHash() {
        resetDb();

        //1、增改
        //hset<key><field ><value > 给 < key > 集合中的 < field > 键赋值 < value >  。返回set成功的数量
        long hsetCount = jedis.hset("user1", "name", "zhangsan");
        log.warn("hsetCount=={}", hsetCount);    //hsetCount==1
        hsetCount = jedis.hset("user2", MapUtil.builder("name", "zhangsan").put("addr", "上海").build());
        log.warn("hsetCount=={}", hsetCount);    //hsetCount==2
        //hmset<key1><field1 ><value1 ><field2 ><value2 > 批量设置 hash 的值, 返回状态
        String resCode = jedis.hmset("user1", MapUtil.builder("password", "1234").put("age", "18").build());
        log.warn("resCode=={}", resCode);    //resCode==OK
        //hsetnx<key><field ><value > 将哈希表 key 中的域 field 的值设置为 value , 当且仅当域 field 不存在。返回set成功的数量
        hsetCount = jedis.hsetnx("user1", "password", "5678");
        log.warn("hsetCount=={}", hsetCount);    //hsetCount==0
        hsetCount = jedis.hsetnx("user1", "addr", "北京");
        log.warn("hsetCount=={}", hsetCount);    //hsetCount==1

        //2 查
        //hget<key1><field > 从 < key1 > 集合 < field > 取出 value
        String hget = jedis.hget("user1", "name");
        log.warn("hget=={}", hget);         //hget==zhangsan
        //hgetAll <key1> 从 < key1 > 集合取出 Map<field, value>
        Map<String, String> user1Map = jedis.hgetAll("user1");
        log.warn("user1Map=={}", user1Map);    //user1Map=={name=zhangsan,  addr=北京,  password=1234,  age=18}
        //hkeys<key> 列出该 hash 集合的所有 field hvals <key > 列出该 hash 集合的所有 value
        Set<String> user1KeySet = jedis.hkeys("user1");
        log.warn("user1KeySet=={}", user1KeySet);    //user1KeySet==[name,  addr,  password,  age]
        //hexists<key1><field > 查看哈希表 key 中, 给定域 field 是否存在。
        boolean hexists = jedis.hexists("user1", "name");
        log.warn("hexists=={}", hexists);    //hexists==true
        //3 增
        //hincrby<key><field ><n > 为哈希表 key 中的域 field 的值加上增量 n
        long age = jedis.hincrBy("user1", "age", 1);
        log.warn("age=={}", age);    //age==19
        //hincrbyFloat <key><field><n>                    为哈希表 key 中的域 field 的值加上增量浮点数n
        double age2 = jedis.hincrByFloat("user1", "age", 1.1);
        log.warn("age2=={}", age2);    //age2==20.1
    }

    @GetMapping("templateHash")
    private void templateHash() {
        resetDb2();
        // 略
    }

    @GetMapping("jedisZset")
    private void jedisZset() {
        resetDb();
        // 略
    }

    @GetMapping("templateZset")
    private void templateZset() {
        resetDb2();
        // 略
    }

    /**
     * 6 Bitmaps 位图  每日访问的用户
     * 可用版本： >= 2.2.0
     * 底层实现是String
     *
     * 6.1.1.简介
     * 现代计算机用二进制（位）作为信息的基础单位，1个字节等于8位，
     * 例如“abc”字符串是由3个字节组成，但实际在计算机存储时将其用二进制表示，“abc”分别对应的ASCII码分别是97、98、99，对应的二进制分别是01100001、01100010和01100011，
     * 如下图
     * 合理地使用操作位能够有效地提高内存使用率和开发效率。
     * Redis提供了Bitmaps这个“数据类型”可以实现对位的操作：
     * （1）Bitmaps本身不是一种数据类型，实际上它就是字符串（key-value），但是它可以对字符串的位进行操作。
     * （2）Bitmaps单独提供了一套命令，所以在Redis中使用Bitmaps和使用字符串的方法不太相同。可以把Bitmaps想象成一个以位为单位的数组，数组的每个单元只能存储0和1，数组的下标在Bitmaps中叫做偏移量。
     */
    @GetMapping("jedisBitmaps")
    private void jedisBitmaps() {
        resetDb();
        Boolean bool;

        // 1、setbit <key><offset><value>                设置 Bitmaps 中某个偏移量的值（0 或 1）。返回set之前的值
        // A————ASCⅡ:65————0100_0001
        bool = jedis.setbit("keyA", 0, false);
        bool = jedis.setbit("keyA", 1, true);
        bool = jedis.setbit("keyA", 2, false);
        bool = jedis.setbit("keyA", 3, false);

        bool = jedis.setbit("keyA", 4, false);
        bool = jedis.setbit("keyA", 5, false);
        bool = jedis.setbit("keyA", 6, false);
        bool = jedis.setbit("keyA", 7, true);


        String keyAType = jedis.type("keyA");
        String keyAValue = jedis.get("keyA");
        //Bitmaps 本身不是一种数据类型，实际上它就是字符串（key-value），但是它可以对字符串的位进行操作。
        log.warn("keyAType=={}, keyAValue=={}", keyAType, keyAValue);   // keyAType==string, keyAValue==A

        // 2、getbit <key><offset>                              获取 Bitmaps 中某个偏移量的值
        Boolean keyA = jedis.getbit("keyA", 1);
        log.warn("keyA=={}", keyA);                             // keyA==true

        // B————ASCⅡ:66————0100_0010
        jedis.set("keyB", "B"); // 通过字符串的方式setbit
        Boolean keyB0 = jedis.getbit("keyB", 0);
        Boolean keyB1 = jedis.getbit("keyB", 1);
        //Bitmaps 本身不是一种数据类型，实际上它就是字符串（key-value），但是它可以对字符串的位进行操作。
        log.warn("keyB0=={}, keyB1=={}", keyB0, keyB1);         // keyB0==false, keyB1==true

        // 3、bitcount <key>[start end]                        统计字符串从 start 字节到 end 字节比特值为 1 的数量，start 和 end 参数 的设置，都可以使用负数值：比如 -1 表示最后一个位，而 -2 表示倒数第二个位， start、end 是指 bit 组的字节的下标数，二者皆包含。
        Long keyALong;
        keyALong = jedis.bitcount("keyA");
        log.warn("keyALong=={}", keyALong);  // keyALong==2
        keyALong = jedis.bitcount("keyA", 1, 7);
        log.warn("keyALong=={}", keyALong); // keyALong==0   这个没搞懂，为啥是0,各种百度也没找到答案，难道是bug？


        // 4、bitop and(or/not/xor) <destkey> [key…]         bitop 是一个复合操作， 它可以做多个 Bitmaps 的 and（交集） 、 or（并集） 、 not （非） 、 xor（异或） 操作并将结果保存在 destkey 中。
        // A————ASCⅡ:65————0100_0001
        // B————ASCⅡ:66————0100_0010
        // AND              0100_0000————ASCⅡ:64————@
        // OR               0100_0011————ASCⅡ:67————C
        // XOR              0000_0011————ASCⅡ:3————
        // NOT A            1011_1110————ASCⅡ:190———�
        jedis.bitop(BitOP.AND, "keyAnd", "keyA", "keyB");
        jedis.bitop(BitOP.OR, "keyOr", "keyA", "keyB");
        jedis.bitop(BitOP.XOR, "keyXor", "keyA", "keyB");
        jedis.bitop(BitOP.NOT, "keyNot", "keyA");       // not只能是一个key
        log.warn("keyAnd=={}", jedis.get("keyAnd"));        // keyAnd==@
        log.warn("keyOr=={}", jedis.get("keyOr"));          // keyOr==C
        log.warn("keyXor=={}", jedis.get("keyXor"));        // keyXor==
        log.warn("keyNot=={}", jedis.get("keyNot"));        // keyNot==�
    }

    /**
     * 6.2 HyperLogLog 大数据基数统计
     * 底层实现是String
     * 可用版本： >= 2.8.9
     *
     * 6.2.1.简介
     * 在工作当中，我们经常会遇到与统计相关的功能需求，比如统计网站PV(PageView页面访问量),可以使用Redis 的incr、incrby轻松实现。
     * 但像UV (UniqueVisitor，独立访客)、独立IP数、搜索记录数等需要去重和计数的问题如何解决?这种求集合中不重复元素个数的问题称为基数问题。
     * 解决基数问题有很多种方案:
     * (1）数据存储在MySQL表中，使用distinct count计算不重复个数
     * (2)使用Redis提供的 hash、set、bitmaps等数据结构来处理
     * 以上的方案结果精确，但随着数据不断增加，导致占用空间越来越大，对于非常大的数据集是不切实际的。
     * 能否能够降低一定的精度来平衡存储空间?Redis推出了HyperLogLog
     * Redis HyperLogLog 是用来做基数统计的算法，HyperLogLog的优点是，在输入元素的数量或者体积非常非常大时，计算基数所需的空间总是固定的、并且是很小的。
     * 在Redis里面，每个 HyperLogLog键只需要花费12 KB内存，就可以计算接近2^64个不同元素的基数。这和计算基数时，元素越多耗费内存就越多的集合形成鲜明对比。
     */
    @GetMapping("jedisHyperLogLog")
    private void jedisHyperLogLog() {
        resetDb();

        // 设置两个key
        final String key1 = "HyperLogLog:" + DateUtil.formatDate(DateUtil.date()) + ":userId1";
        final String key2 = "HyperLogLog:" + DateUtil.formatDate(DateUtil.date()) + ":userId2";
        final String keyMonthMerge = "HyperLogLog:" + DateUtil.formatDate(DateUtil.date()) + ":userIdMonthMerge";
        Long addStatus, count;

        // 1、增改
        // pfadd <key> [element ...] 添加指定元素到 HyperLogLog 中。整数回复： 如果 HyperLogLog 的内部储存被修改了， 那么返回 1 ， 否则返回 0 。
        addStatus = jedis.pfadd(key1, "a");         // addStatus==1
        log.warn("addStatus=={}", addStatus);
        addStatus = jedis.pfadd(key1, "a");         // addStatus==0
        log.warn("addStatus=={}", addStatus);
        addStatus = jedis.pfadd(key2, "b");         // addStatus==1
        log.warn("addStatus=={}", addStatus);

        // 底层实现是String
        log.warn("keyType=={}", jedis.type(key1));              // keyType==string

        // 2、合并
        // pfmerge <destkey> [sourcekey ...] 将一个或多个 key 合并后的结果存 储在另一个 key 中，比如每月活跃用户可以使用每天的活跃用户来合并计算可得。返回状态
        String mergeStatus = jedis.pfmerge(keyMonthMerge, key1, key2);
        log.warn("mergeStatus=={}", mergeStatus);               // mergeStatus==OK

        // 3、查
        // pfcount <key> [key ...] 计算 的近似基数
        count = jedis.pfcount(key1);
        log.warn("count=={}", count);               // count==1
        count = jedis.pfcount(key2);
        log.warn("count=={}", count);               // count==1
        count = jedis.pfcount(keyMonthMerge);
        log.warn("count=={}", count);               // count==2

        // pfCount 研究
        for (int i = 1; i <= 10000; i++) {
            jedis.pfadd(key1, "" + RandomUtil.randomInt(10));           // 不会超过  10 或 10000 的近似值，因为会有大量重复的会pfadd失败。
            jedis.pfadd(key2, "" + RandomUtil.randomInt(10000_0000));   // 不会超过 一亿 或 10000 的近似值。
        }
        log.warn("key1Count=={}", jedis.pfcount(key1));             // key1Count==11
        log.warn("key2Count=={}", jedis.pfcount(key2));             // key2Count==9967
    }

    /**
     * 6.3 Geospatial 地理坐标
     * 底层实现是 zSet
     * 可用版本： >= 3.2.0
     */
    @GetMapping("jedisGeospatial")
    private void jedisGeospatial() {
        resetDb();

        Long addCount;
        // 1、增改
        // geoadd<key>< longitude><latitude><member> [longitude latitude member...]         添加地理位置（经度，纬度，名称）.返回add的数量
        //   两极无法直接添加，一般会下载城市数据，直接通过Java程序一次性导入。
        //   有效的经度从-180度到180度。有效的纬度从-85.05112878度到85.05112878度。当坐标位置超出指定范围时，该命令将会返回一个错误。
        //   已经添加的数据，是无法再次往里面添加的。
        addCount = jedis.geoadd("china:city", 116.433547, 39.909462, "北京站");
        log.warn("addCount=={}", addCount);     // addCount==1
        addCount = jedis.geoadd("china:city", 116.433547, 39.909462, "北京站");// 改
        log.warn("addCount=={}", addCount);     // addCount==0


        Map<String, GeoCoordinate> map = new TreeMap<>();
        map.put("天津站", new GeoCoordinate(117.216853, 39.142488));
        map.put("上海站", new GeoCoordinate(121.462056, 31.255923));

        GeoAddParams geoAddParams = new GeoAddParams().nx().xx().ch();
//        jedis.geoadd("china:city",geoAddParams, map);
        addCount = jedis.geoadd("china:city", map);
        log.warn("addCount=={}", addCount);     // addCount==2

        // 底层实现是 zSet
        log.warn("type=={}", jedis.type("china:city"));     // type==zset

        //jedis.geoadd("china:city", 116.433547, 86, "纽约");     // ERR invalid longitude,latitude pair 116.433547,86.000000

        // 2、查
        // geopos <key><member> [member...]         获得指定地区的坐标值
        List<GeoCoordinate> geopos = jedis.geopos("china:city", "北京站", "天津站");
        log.warn("geopos=={}", geopos);     // geopos==[(116.43354803323746,39.909462205758736), (117.21685498952866,39.14248853431784)]  与 add有微小差异

        // geodist<key><member1 ><member2 > [m | km | ft | mi ]         获取两个位置之间的直线距离
        // 单位：
        // m 表示单位为米[默认值]。
        // km 表示单位为千米。
        // mi 表示单位为英里。
        // ft 表示单位为英尺。
        Double geodist = jedis.geodist("china:city", "北京站", "天津站");
        Double geodistKm = jedis.geodist("china:city", "北京站", "上海站", GeoUnit.KM);
        log.warn("geodist=={}", geodist);           // geodist==108596.9107
        log.warn("geodistKm=={}", geodistKm);       // geodistKm==1064.0813

        // georadius <key> <longitude><latitude> radius m|km|ft|mi      以给定的经纬度为中心， 找出某一半径内的元素。经度 纬度 距离 单位
        //    WITHDIST: 在返回位置元素的同时， 将位置元素与中心之间的距离也一并返回。 距离的单位和用户给定的范围单位保持一致。
        //    WITHCOORD: 将位置元素的经度和维度也一并返回。
        //    WITHHASH: 以 52 位有符号整数的形式， 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项主要用于底层应用或者调试， 实际中的作用并不大。
        //    命令默认返回未排序的位置元素。 通过以下两个参数， 用户可以指定被返回位置元素的排序方式：
        //    ASC: 根据中心的位置， 按照从近到远的方式返回位置元素。
        //    DESC: 根据中心的位置， 按照从远到近的方式返回位置元素。
        //    在默认情况下， GEORADIUS 命令会返回所有匹配的位置元素。 虽然用户可以使用 COUNT <count> 选项去获取前 N 个匹配元素， 但是因为命令在内部可能会需要对所有被匹配的元素进行处理， 所以在对一个非常大的区域进行搜索时， 即使只使用 COUNT 选项去获取少量元素， 命令的执行速度也可能会非常慢。 但是从另一方面来说， 使用 COUNT 选项去减少需要返回的元素数量， 对于减少带宽来说仍然是非常有用的。
        List<GeoRadiusResponse> georadius1 = jedis.georadius("china:city", 116.433547, 39.909462, 200, GeoUnit.KM);
        List<GeoRadiusResponse> georadius2 = jedis.georadius("china:city", 116.433547, 39.909462, 2000, GeoUnit.KM);
        List<GeoRadiusResponse> georadius3 = jedis.georadius("china:city", 116.433547, 39.909462, 2000, GeoUnit.KM,
            GeoRadiusParam.geoRadiusParam()
                .withCoord()    // 经纬度坐标
                .withDist()     // 距离，米
                .withHash()     //
                .sortDescending()
                .count(100)
        );
        log.warn("georadius1=={}", HuJsonUtil.toJsonPrettyStr(georadius1));
        log.warn("georadius2=={}", HuJsonUtil.toJsonPrettyStr(georadius2));
        log.warn("georadius3=={}", HuJsonUtil.toJsonPrettyStr(georadius3));
        //georadius1==
        // [ {
        //  "member" : "5YyX5Lqs56uZ",
        //  "distance" : 0.0,
        //  "coordinate" : null,
        //  "rawScore" : 0,
        //  "memberByString" : "北京站"
        //}, {
        //  "member" : "5aSp5rSl56uZ",
        //  "distance" : 0.0,
        //  "coordinate" : null,
        //  "rawScore" : 0,
        //  "memberByString" : "天津站"
        //} ]

        //georadius2==[ {
        //  "member" : "5LiK5rW356uZ",
        //  "distance" : 0.0,
        //  "coordinate" : null,
        //  "rawScore" : 0,
        //  "memberByString" : "上海站"
        //}, {
        //  "member" : "5aSp5rSl56uZ",
        //  "distance" : 0.0,
        //  "coordinate" : null,
        //  "rawScore" : 0,
        //  "memberByString" : "天津站"
        //}, {
        //  "member" : "5YyX5Lqs56uZ",
        //  "distance" : 0.0,
        //  "coordinate" : null,
        //  "rawScore" : 0,
        //  "memberByString" : "北京站"
        //} ]

        //georadius3==[ {
        //  "member" : "5YyX5Lqs56uZ",
        //  "distance" : 1.0E-4,
        //  "coordinate" : {
        //    "longitude" : 116.43354803323746,
        //    "latitude" : 39.909462205758736
        //  },
        //  "rawScore" : 4069885676106521,
        //  "memberByString" : "北京站"
        //}, {
        //  "member" : "5aSp5rSl56uZ",
        //  "distance" : 108.5969,
        //  "coordinate" : {
        //    "longitude" : 117.21685498952866,
        //    "latitude" : 39.14248853431784
        //  },
        //  "rawScore" : 4069186981370726,
        //  "memberByString" : "天津站"
        //}, {
        //  "member" : "5LiK5rW356uZ",
        //  "distance" : 1064.0813,
        //  "coordinate" : {
        //    "longitude" : 121.46205693483353,
        //    "latitude" : 31.255924167775554
        //  },
        //  "rawScore" : 4054802101354129,
        //  "memberByString" : "上海站"
        //} ]


    }

    /** 上货：改库存 */
    @GetMapping("skuUpdate/")
    private String skuUpdate() {
        jedis.flushDB();
        log.info("上货, count=={}", 100);
        jedis.set("sku:101:count", "" + 100);
        return "改库存为 100";
    }

    @GetMapping("skuSecondKill/{userId}")
    private String skuSecondKill(@PathVariable String userId) {
//        log.info("uId=={}",  uId);

        // 1、参数校验：略

        // 2、获取 redis 连接
//        每次new 一个新的客户端, 模拟并发
//        Jedis jedis = new Jedis("localhost",  6379);
        Jedis jedis = jedisPool.getResource();

        // 3、定义key
        String skuCountStrKey = "sku:101:count";        // 库存数量
        String successUserSetKey = "sku:101:user";      // 秒杀成功的用户id

        // 开启乐观锁 监控 skuCountStrKey。相当于mysql 的 version 字段
        String watchCode = jedis.watch(skuCountStrKey);
//        log.info("watchCode=={}", watchCode);  // watchCode==OK

        // 4、获取库存, 如果为null 说明秒杀还未开始。可以在抢购页面禁用按钮
        String skuCount = jedis.get(skuCountStrKey);
        if (StrUtil.isEmpty(skuCount)) {
            jedis.close();
            return "NOT_START, 秒杀还未开始";
        }

        // 5、如果库存 ≤ 0, 秒杀结束
        skuCount = jedis.get(skuCountStrKey);
        if (Integer.valueOf(skuCount) <= 0) {
            jedis.close();
            return "OVER, 秒杀结束,  userId==" + userId;
        }

        // 6、判断同一个用户重复秒杀
        boolean containsUser = jedis.sismember(successUserSetKey, userId);
        if (containsUser) {
            jedis.close();
            return "REPEAT, 您已秒杀成功了, 不要重复秒杀";
        }

        // 7 秒杀成功，库存-1，记录秒杀成功的用户
//        Long decr = jedis.decr(skuCountStrKey);
//        jedis.sadd(successUserSetKey, userId);  // 将秒杀成功的放入set

        Transaction multi = jedis.multi();
        multi.decr(skuCountStrKey);
        multi.sadd(successUserSetKey, userId);  // 将秒杀成功的放入set
        List<Object> result = multi.exec();
        if (result == null || result.size() == 0) {
            jedis.close();
            return "FAIL, 秒杀失败, userId==" + userId;
        }

        jedis.close();
        return "SUCCESS, 秒杀成功, userId==" + userId + ",  剩余数量==" + result.get(0);
    }

    /** 停止打印RDB上次持久化的时间 */
    private boolean stopPrintLastSaveDate;

    /** 打印RDB上次持久化的时间，每隔一秒 */
    @SneakyThrows
    @GetMapping("printLastSaveDate/{stopPrintLastSaveDate}")
    private void printLastSaveDate(@PathVariable(required = false) boolean stopPrintLastSaveDate) {
        jedis.flushDB();
        this.stopPrintLastSaveDate = stopPrintLastSaveDate;

        while (true) {
            if (this.stopPrintLastSaveDate) {
                break;
            }
            DateTime lastsaveDate = DateTime.of(1000 * jedis.lastsave());
            log.info("lastsaveDate=={}", lastsaveDate);
            TimeUnit.SECONDS.sleep(1);
        }
    }

    /**
     * 【12. 持久化之RDB（Redis DataBase）全量备份】
     * 【12.2.8 如何触发 RDB 快照】1、自动备份（定期快照）
     * 在指定的时间间隔内将内存中的数据集快照写入磁盘， 也就是行话讲的 Snapshot 快 照，它恢复时是将快照文件直接读到内存里
     *
     * RDB配置内容位置：windows 配置文件名称 redis.windows-service.conf 或 redis.windows.conf
     * ################################ SNAPSHOTTING  ################################
     * 1、自动备份周期、changes。  每 <seconds>检查一次，如果有 <changes> 个变化（增删改）则持久化备份一次
     * #   save <seconds> <changes>
     * redis5 默认 自动备份周期、changes
     * save 900 1
     * save 300 10
     * save 60 10000
     *
     * 2、当 Redis 无法写入磁盘的话，直接关掉 Redis 的写操作。推荐 yes.
     * stop-writes-on-bgsave-error yes
     *
     * 3、开启rdb备份压缩。
     * 对于存储到磁盘中的快照，可以设置是否进行压缩存储。如果是的话，redis 会采用 LZF 算法进行压缩。
     * 如果你不想消耗 CPU 来进行压缩的话，可以设置为关闭此功能。推荐 yes.
     * rdbcompression yes
     *
     * 4、 rdbchecksum 检查完整性
     * 在存储快照后，还可以让 redis 使用 CRC64 算法来进行数据校验， 但是这样做会增加大约 10%的性能消耗，
     * 如果希望获取到最大的性能提升，可以关 闭此功能。推荐 yes.
     * rdbchecksum true
     *
     * 5、备份文件的名字
     * dbfilename dump.rdb
     * 6、备份文件的路径位置
     * dir ./
     */
    @SneakyThrows
    @GetMapping("testRDBAuto")
    private String testRDBAuto() {
        // 为了查看测试效果，修改配置，增加一行自动备份周期  save 30 3

        int randomInt = RandomUtil.randomInt(1, 3);
        // 增改删都算 changes
        String statusCode = jedis.set("key" + randomInt, RandomUtil.randomString(10));// 增改
        return statusCode;
    }


    /**
     * 【12. 持久化之RDB（Redis DataBase）全量备份】
     * 【12.2.8 如何触发 RDB 快照】2、手动触发RDB备份
     *
     * 12.2.8.2. 命令 save VS bgsave
     * save ：save 时只管保存，其它不管，全部阻塞。手动保存。不建议。
     * bgsave：Redis 会在后台异步进行快照操作，快照同时还可以响应客户端请求。
     * 可以通过 lastsave 命令获取最后一次成功执行快照的时间
     *
     * 12.2.8.3. flushall 命令
     * 执行 flushall 命令，也会产生 dump.rdb 文件，但里面是空的，无意义
     */
    @SneakyThrows
    @GetMapping("testRDBManual/{type}")
    private String testRDBManual(@PathVariable int type) {
        // 测试前现 调用 printLastSaveDate()
        String statusCode;
        if (type == 1) {
            statusCode = jedis.save();
        } else if (type == 2) {
            statusCode = jedis.bgsave();
        } else if (type == 3) {
            statusCode = jedis.flushAll();
        } else {
            statusCode = "";
        }

        return statusCode;
    }

    /**
     * 【RDB优缺点】
     * 12.2.9. 优势
     *  空间，节省磁盘空间
     *  时间，恢复速度快
     *  适合大规模的数据恢复
     *  对数据完整性和一致性要求不高更适合使用
     * 12.2.10. 劣势
     *  Fork 的时候，内存中的数据被克隆了一份，大致 2 倍的膨胀性需要考虑
     *  虽然 Redis 在 fork 时使用了写时拷贝技术,但是如果数据庞大时还是比较消 耗性能。
     *  在备份周期在一定间隔时间做一次备份，所以如果 Redis 意外 down 掉的话， 就会丢失最后一次快照后的所有修改。
     */
    private static void rdbFeatures(String[] args) {
    }

    /**
     * 【13. 持久化之AOF（Append Only File）增量备份】
     * 以日志的形式来记录每个写操作（增量保存），将 Redis 执行过的所有写指令记录下 来(读操作不记录)，只许追加文件但不可以改写文件，redis 启动之初会读取该文件重 新构建数据，
     * 换言之，redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作
     *
     * AOF配置内容位置：windows 配置文件名称 redis.windows-service.conf 或 redis.windows.conf
     * ############################## APPEND ONLY MODE ###############################
     *
     * 1、AOF持久化开关
     * appendonly no
     *
     * 2、AOF持久化文件名字
     * appendfilename "appendonly.aof"
     *
     * 3、AOF持久化文件路径位置:与RDB配置一致
     * dir ./
     *
     * 4、持久化策略
     * 4.1 always 始终同步，每次 Redis 的写入都会立刻记入日志；性能较差但数据完整性比较好
     * 4.2 everysec 每秒同步，每秒记入日志一次，如果宕机，本秒的数据可能丢失。
     * 4.3 no 不主动进行同步，把同步时机交给操作系统。
     * # appendfsync always
     * appendfsync everysec
     * # appendfsync no
     *
     * 5、重写AOF文件，重写的成RDB格式。redis4.0 版本后的重写，实际上就是重写成 rdb 的二进制快照格式，以二级制的形式附 在新的 aof 头部，作为已有的历史数据，替换掉原来的流水账操作。
     * 5.1 如果 no-appendfsync-on-rewrite=yes ,不写入aof文件，只写入缓存，用户请求不会阻塞，但是在这段时间如果宕机会丢失这段时间的缓存数据。（降低数据安全性，提高性能）
     * no-appendfsync-on-rewrite no
     *
     * 6、触发重写条件
     * 6.1 触发自动重写。 如果 Redis 的 AOF当前大小 >= base_size + base_size * 100% (默认) 且当前大小 >= 64mb(默认)的情况下，Redis会对 AOF文件 进行重写。
     * 设置重写的基准值，文件达到 100%时开始重写（文件是原来重写后文件的 2 倍时触发）
     * auto-aof-rewrite-percentage 100
     * 设置重写的基准值，最小文件 64MB。达到这个值开始重写
     * auto-aof-rewrite-min-size 64mb
     * 6.2 触发手动重写
     * bgRewriteAOF 命令
     */
    @GetMapping("testAOFRewriteAuto")
    private void testAOFRewriteAuto() {
        //为了查看测试效果，设置 auto-aof-rewrite-min-size 4mb，每次删除备份文件 appendonly.aof
        jedis.flushDB();
        int strLen = 1 * 1024 * 1024 - 1; // 1MB-1B

        for (int i = 0; i < 32; i++) {
            log.debug("{}", i);
//            String randomString = RandomUtil.randomString(strLen);
            String randomString = RandomUtil.randomString("A", strLen);   // 全是一样的字符，AOF压缩率极高，压缩后的AOF在用zip压缩，很能压缩很多
            template.opsForValue().set("key" + i, randomString);
        }
    }

    /**
     * 【13. 持久化之AOF（Append Only File）增量备份】
     * 6.2 触发手动重写
     * bgRewriteAOF 命令
     */
    @GetMapping("testAOFRewriteManual")
    private String testAOFRewriteManual() {
        String statusCode = jedis.bgrewriteaof();
        return statusCode;
    }

    /** redis分布式锁-不使用锁，造成问题 */
    @GetMapping("testNoLock")
    private void testNoLock() {
        Jedis jedis = jedisPool.getResource();

//        不用数据库自增，无法测试分布式锁的效果
//        Long num = jedis.incr("num");

        // 使用java程序，测试分布式锁的效果
        String numNoLock = jedis.get("numNoLock");
        if (numNoLock == null) {
            jedis.set("numNoLock", "1");
        } else {
            String numIncr = String.valueOf(Integer.parseInt(numNoLock) + 1);
            jedis.set("numNoLock", numIncr);
        }

        jedis.close();
    }

    /**
     * redis分布式锁-使用锁
     *
     * 【依然存在问题】当前实现的分布式锁可以满足大部分一般业务需求，但是在极端高并发场景，依然存在问题。使用成熟方案 RedisSon 解决
     * 1、不可重试
     * 获取锁只尝试一次就返回false，没有重试机制
     * 2、不可重入
     * 同一个线程无法多次获取同一把锁（比如A方法调用B方法，A用了锁B也用这把锁）
     * 3、超时释放
     * 锁超时释放虽然可以避免死锁，但如果是业务执行耗时较长，也会导致锁释放，存在安全隐患。超时时间太长又影响并发性能
     * 4、主从一致性
     * 如果Redis提供了主从集群,主从同步存在延迟，当主宕机时，如果 从 并未同步 主 中的锁数据，则会出现多个线程获取到锁
     */
    @GetMapping("testLock")
    @SneakyThrows
    private void testLock() {
        Jedis jedis = jedisPool.getResource();
        final long expireSeconds = 2L;  // 锁有效期。设置一个靠谱的时间，不然业务逻辑未完成解锁了
        // 锁标识，标识是自己创建的，删除时判断，防止删除其它线程的。
        // 必须用UUID这类唯一标识。如果用线程id，因为不同jvm的线程id可能一样所以会重复出现问题
        String curUUID = IdUtil.simpleUUID();

        //上锁（拿锁）  ex设置一个靠谱的时间，不然业务逻辑未完成解锁了
        String status = jedis.set("lock", curUUID, new SetParams().nx().ex(expireSeconds));

        // 上锁成功
        if ("OK".equalsIgnoreCase(status)) {
            // 模拟业务阻塞（10%的概率）场景，阻塞时长 超过了 锁有效期EX（expireSeconds），会出现问题
            if (RandomUtil.randomInt(10) == 1) {
                TimeUnit.MILLISECONDS.sleep(2010);
            }

            // 不用数据库自增，无法测试分布式锁的效果
//            Long numLock = jedis.incr("numLock");

            // 使用java程序，测试分布式锁的效果
            String numLock = jedis.get("numLock");
            if (numLock == null) {
                jedis.set("numLock", "1");
            } else {
                String numIncr = String.valueOf(Integer.parseInt(numLock) + 1);
                jedis.set("numLock", numIncr);
            }


            /** 判断标识是UUID是自己线程创建的则删除，先查再删 */
            // 方式1: 没用事务，有线程安全问题
            /*
            String redisUUID = jedis.get("lock");
            if (curUUID.equals(redisUUID)) {
                //释放锁
                jedis.del("lock");
            }*/

            // 方式2: 用支持事务的lua脚本
            String txLuaScript = ResourceUtil.readUtf8Str("lua/delLockTx.lua");
            Object result = jedis.eval(txLuaScript, Arrays.asList("lock"), Arrays.asList(curUUID));
        } else {
            // 获取锁失败
            // 1. 获取锁失败处理方案1：无限重试，直到获取锁成功。比较耗资源
            try {
                TimeUnit.MILLISECONDS.sleep(100);
                testLock();  // 递归调用，直到拿到锁
            } catch (InterruptedException e) {
                log.warn("异常", e);
            }
            // 2. 获取锁失败处理方案2：返回失败(如：失败json信息/跳转失败页面)
            // return;
        }

        jedis.close();
    }

    /**
     * 【Redisson分布式锁】 优化testLock()
     * 满足多进程可见、互斥、高可用、高性能、安全性的特性外，解决以下4个遗留问题
     * 1、不可重试
     * 获取锁只尝试一次就返回false，没有重试机制
     * 2、不可重入
     * 同一个线程无法多次获取同一把锁（比如A方法调用B方法，A用了锁B也用这把锁）
     * 3、超时释放
     * 锁超时释放虽然可以避免死锁，但如果是业务执行耗时较长，也会导致锁释放，存在安全隐患。超时时间太长又影响并发性能
     * 4、主从一致性
     * 如果Redis提供了主从集群,主从同步存在延迟，当主宕机时，如果 从 并未同步 主 中的锁数据，则会出现多个线程获取到锁
     *
     * 1、tryLock(long waitTime, long leaseTime, TimeUnit unit)
     * 【阻塞直到拿到锁或超过等待时长】租期为-1（底层为看门狗模式，租期（ex）为30秒，会自动修改租期为30秒（默认30秒）直到执行 unLock()。
     * leaseTime：租期（set ex），默认为-1(底层30秒，通过看门狗模式保证在解锁)，相当于 tryLock(long time, TimeUnit unit)
     * waitTime：等待时长，默认值为 0，相当于 tryLock()
     * 2、lock(long leaseTime, TimeUnit unit)
     * 【阻塞直到拿到锁】租期为-1（底层为看门狗模式，租期（ex）为30秒，会自动修改租期为30秒（默认30秒）直到执行 unLock()。
     */
    @SneakyThrows
    @GetMapping("testRedissonRLock1")
    private void testRedissonRLock1() {
        Jedis jedis = jedisPool.getResource();
        // 创建可重入锁
        RLock rLock = redissonClient.getLock("rLock1");
        // 阻塞直到拿到锁
        rLock.lock();

        // 模拟业务阻塞。不需要了，业务阻塞下一个线程就拿不到锁
//        if (RandomUtil.randomInt(10) == 1) {
//            TimeUnit.MILLISECONDS.sleep(2010);
//        }

        // 使用java程序，测试分布式锁的效果
        String numRLock1 = jedis.get("numRLock1");
        if (numRLock1 == null) {
            jedis.set("numRLock1", "1");
        } else {
            String numIncr = String.valueOf(Integer.parseInt(numRLock1) + 1);
            jedis.set("numRLock1", numIncr);
        }

        /** 业务完成，解锁 */
        rLock.unlock();
        jedis.close();
    }

    /**
     * 【Redisson分布式锁】 优化testLock()
     * 满足多进程可见、互斥、高可用、高性能、安全性的特性外，解决以下4个遗留问题
     * 1、不可重试
     * 获取锁只尝试一次就返回false，没有重试机制
     * 2、不可重入
     * 同一个线程无法多次获取同一把锁（比如A方法调用B方法，A用了锁B也用这把锁）
     * 3、超时释放
     * 锁超时释放虽然可以避免死锁，但如果是业务执行耗时较长，也会导致锁释放，存在安全隐患。超时时间太长又影响并发性能
     * 4、主从一致性
     * 如果Redis提供了主从集群,主从同步存在延迟，当主宕机时，如果 从 并未同步 主 中的锁数据，则会出现多个线程获取到锁
     *
     * 1、tryLock(long waitTime, long leaseTime, TimeUnit unit)
     * 【阻塞直到拿到锁或超过等待时长】租期为-1（底层为看门狗模式，租期（ex）为30秒，会自动修改租期为30秒（默认30秒）直到执行 unLock()。
     * leaseTime：租期（set ex），默认为-1(底层30秒，通过看门狗模式保证在解锁)，相当于 tryLock(long time, TimeUnit unit)
     * waitTime：等待时长，默认值为 0，相当于 tryLock()
     * 2、lock(long leaseTime, TimeUnit unit)
     * 【阻塞直到拿到锁】租期为-1（底层为看门狗模式，租期（ex）为30秒，会自动修改租期为30秒（默认30秒）直到执行 unLock()。
     */
    @SneakyThrows
    @GetMapping("testRedissonRLock2")
    private void testRedissonRLock2() {
        Jedis jedis = jedisPool.getResource();
        // 创建可重入锁
        RLock rLock = redissonClient.getLock("rLock2");

        // 阻塞直到拿到锁或超时，60秒
        boolean lockSuccess = rLock.tryLock(60, TimeUnit.SECONDS);

        if (!lockSuccess) {
            log.warn("【获取锁超时】可能当前人数较多(或重复下单等), 稍后再试");
        }

        // 模拟业务阻塞。不需要了，业务阻塞下一个线程就拿不到锁
//        if (RandomUtil.randomInt(10) == 1) {
//            TimeUnit.MILLISECONDS.sleep(2010);
//        }

        // 使用java程序，测试分布式锁的效果
        String numRLock = jedis.get("numRLock2");
        if (numRLock == null) {
            jedis.set("numRLock2", "1");
        } else {
            String numIncr = String.valueOf(Integer.parseInt(numRLock) + 1);
            jedis.set("numRLock2", numIncr);
        }

        /** 业务完成，解锁 */
        rLock.unlock();
        jedis.close();
    }

    /**
     * 【推荐红锁】可重入联锁-非红锁-所有的锁都上锁成功才算成功
     * 解决 主从不一致 安全问题
     * 缺点：无法保证高可用。一台redis挂了，不能上锁了
     */
/*    @GetMapping("redissonMultiLock")
    private void redissonMultiLock() throws InterruptedException {
        String lockName = "multiRLock";
        RLock rLock1 = redissonClient1.getLock(lockName);
        RLock rLock2 = redissonClient2.getLock(lockName);
        RLock rLock3 = redissonClient3.getLock(lockName);
        RedissonMultiLock redissonMultiLock = new RedissonMultiLock(rLock1, rLock2, rLock3);
        // 阻塞直到拿到锁。给lock1，lock2，lock3同时加锁，所有的锁都上锁成功才算成功。如果没有手动解开的话，10秒钟后将会自动解开
//        redissonMultiLock.lock();                         // 参数含义 非连锁一致，详见上面的 /testRedissonRLock1()注释
        // 为加锁等待100秒时间，并在加锁成功10秒钟后自动解开
//        boolean bool = redissonMultiLock.tryLock();       // 参数含义 非连锁一致，详见上面的 /testRedissonRLock1()注释
        boolean bool = redissonMultiLock.tryLock(3, TimeUnit.SECONDS);     // 参数含义 非连锁一致，详见上面的 /testRedissonRLock1()注释

        try {
            log.warn("获取联锁状态, bool=={}", bool);
            // 业务逻辑
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            log.error("异常", e);
        } finally {
            redissonMultiLock.unlock();
        }
    }*/

    /**
     * 【推荐红锁】可重入联锁-红锁-过半锁上锁成功就算成功
     * 解决 主从不一致 安全问题
     * 缺点：无法保证锁安全。场景：jvm gc时 ，stop the world，看门狗未及时续期。 具体看视频【马士兵教育】带你深入“神一样的cap定理以及BASE理论“"https://www.bilibili.com/video/BV1vY41147Wu/?spm_id_from=autoNext
     */
/*    @GetMapping("redissonRedLock")
    private void redissonRedLock() throws InterruptedException {
        String lockName = "redMultiRLock";
        RLock rLock1 = redissonClient1.getLock(lockName);
        RLock rLock2 = redissonClient2.getLock(lockName);
        RLock rLock3 = redissonClient3.getLock(lockName);
        RedissonRedLock redissonRedLock = new RedissonRedLock(rLock1, rLock2, rLock3);
        // 阻塞直到拿到锁。给lock1，lock2，lock3同时加锁，过半锁上锁成功就算成功。如果没有手动解开的话，10秒钟后将会自动解开
//        redissonRedLock.lock();                         // 参数含义 非连锁一致，详见上面的 /testRedissonRLock1()注释
        // 为加锁等待100秒时间，并在加锁成功10秒钟后自动解开
//        boolean bool = redissonRedLock.tryLock();       // 参数含义 非连锁一致，详见上面的 /testRedissonRLock1()注释
        boolean bool = redissonRedLock.tryLock(3, TimeUnit.SECONDS);     // 参数含义 非连锁一致，详见上面的 /testRedissonRLock1()注释

        try {
            log.warn("获取联锁状态, bool=={}", bool);
            // 业务逻辑
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            log.error("异常", e);
        } finally {
            redissonRedLock.unlock();
        }
    }*/

    /**
     * 【CAP原则】  https://baike.baidu.com/item/CAP原则
     * CAP原则又称CAP定理，指的是在一个分布式系统中， Consistency（一致性）、 Availability（可用性）、Partition tolerance（分区容错性），三者不可得兼。
     * 一致性（C）：在分布式系统中的所有数据备份，在同一时刻是否同样的值。（等同于所有节点访问同一份最新的数据副本）
     * 可用性（A）：保证每个请求不管成功或者失败都有响应。（跟高可用概念不一样）
     * 分区容忍性（P）：系统中任意信息的丢失或失败不会影响系统的继续运作。 详见 https://www.cnblogs.com/shoshana-kong/p/13854362.html
     * https://www.bilibili.com/video/BV1KT4y1A7Uf?spm_id_from=333.337.search-card.all.click
     */
}